From 5bef0fa8666b8bd5215f1ce85f83ed5518eab87c Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Tue, 5 May 2026 22:46:58 +0100 Subject: [PATCH 01/29] fix: docked queue scroll-to-now-playing never fired on track/group change Both auto-scroll effects checked `open` (the floating queue toggle) which is always false in docked mode. Changed guard to `isActive` so the docked queue correctly follows the current track when it changes and re-centres after a group switch. Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/components/queue/QueueSidebar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/renderer/src/components/queue/QueueSidebar.tsx b/renderer/src/components/queue/QueueSidebar.tsx index 70528e7..30a2e49 100644 --- a/renderer/src/components/queue/QueueSidebar.tsx +++ b/renderer/src/components/queue/QueueSidebar.tsx @@ -122,16 +122,16 @@ export const QueueSidebar = forwardRef(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 From 50cef71b145e6b81b631a816e5398acaea24e1e0 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Tue, 5 May 2026 22:47:14 +0100 Subject: [PATCH 02/29] style: add gradient background to docked queue sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the flat --bg-1 fill with a subtle 175deg gradient (#2c2c32 → #232327 → #1e1e22) and a soft left-edge shadow so the docked panel has some depth against the main content area. Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/QueueSidebar.module.css | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index dbb0068..3adc783 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -43,9 +43,14 @@ border-radius: 0; border: none; border-left: 1px solid var(--border); - box-shadow: none; + box-shadow: -1px 0 40px rgba(0, 0, 0, 0.35); backdrop-filter: none; - background: var(--bg-1); + background: linear-gradient( + 175deg, + #2c2c32 0%, + #232327 40%, + #1e1e22 100% + ); transform: none; transition: none; /* Sit above the TopNav drag region (z-index 199) so the buttons below From 9e70183e41e864ed52cdc397f9f8cd7d24a084ba Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 15:43:42 +0100 Subject: [PATCH 03/29] =?UTF-8?q?style:=20transparent=20docked=20queue=20b?= =?UTF-8?q?ackground=20=E2=80=94=20inherits=20app=20gradient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the custom gradient fill so the docked sidebar shows the shell's radial gradient through, making it feel like one continuous surface rather than a separate panel. Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/QueueSidebar.module.css | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index 3adc783..a73c0dd 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -43,14 +43,9 @@ border-radius: 0; border: none; border-left: 1px solid var(--border); - box-shadow: -1px 0 40px rgba(0, 0, 0, 0.35); + box-shadow: none; backdrop-filter: none; - background: linear-gradient( - 175deg, - #2c2c32 0%, - #232327 40%, - #1e1e22 100% - ); + background: transparent; transform: none; transition: none; /* Sit above the TopNav drag region (z-index 199) so the buttons below From d88a804436b9d86df61f49a35da765d3fb037c32 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 15:44:26 +0100 Subject: [PATCH 04/29] style: boost purple/blue radial gradient prominence in app shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Increased opacity on all three radial stops (0.45→0.7, 0.35→0.55, 0.3→0.45) and shifted hues slightly warmer/deeper so the gradient reads clearly through the transparent docked queue panel. Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/App.module.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/renderer/src/styles/App.module.css b/renderer/src/styles/App.module.css index 7c48b1b..b19590a 100644 --- a/renderer/src/styles/App.module.css +++ b/renderer/src/styles/App.module.css @@ -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; } From 75c7f72a9c8a96ceba74dd37a89a718876b5aa8d Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 15:46:21 +0100 Subject: [PATCH 05/29] =?UTF-8?q?style:=20remove=20docked=20queue=20border?= =?UTF-8?q?-left=20=E2=80=94=20borderless=20floating=20scrollbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/QueueSidebar.module.css | 1 - 1 file changed, 1 deletion(-) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index a73c0dd..caf2af0 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -42,7 +42,6 @@ margin-left: auto; border-radius: 0; border: none; - border-left: 1px solid var(--border); box-shadow: none; backdrop-filter: none; background: transparent; From dc53246a302271dc5da487bbfb32afda2af253e6 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 15:47:33 +0100 Subject: [PATCH 06/29] style: wrap docked queue window controls in a frosted pill Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/components/queue/QueueSidebar.tsx | 4 +++- renderer/src/styles/QueueSidebar.module.css | 13 ++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/renderer/src/components/queue/QueueSidebar.tsx b/renderer/src/components/queue/QueueSidebar.tsx index 30a2e49..1929408 100644 --- a/renderer/src/components/queue/QueueSidebar.tsx +++ b/renderer/src/components/queue/QueueSidebar.tsx @@ -290,7 +290,9 @@ export const QueueSidebar = forwardRef(function Queue )} {isDocked && (
- +
+ +
)}
diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index caf2af0..5672bbe 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -59,11 +59,22 @@ align-items: center; justify-content: flex-end; height: var(--nav-h); - padding: 0 8px; + padding: 0 12px; flex-shrink: 0; -webkit-app-region: drag; } +.dockedTopBar .winPill { + display: flex; + align-items: center; + gap: 2px; + background: rgba(255, 255, 255, 0.07); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 20px; + padding: 3px 4px; + -webkit-app-region: no-drag; +} + .resizeHandle { position: absolute; top: 0; left: 0; bottom: 0; From 10decd6f5eab70a596ef6d3ffcc6c7e6ce315682 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 15:48:43 +0100 Subject: [PATCH 07/29] style: fatter pill scrollbar on queue content area Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/QueueSidebar.module.css | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index 5672bbe..d63a77f 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -171,6 +171,22 @@ padding: 6px 6px 24px; } +.content::-webkit-scrollbar { + width: 6px; +} +.content::-webkit-scrollbar-track { + background: transparent; +} +.content::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.18); + border-radius: 99px; + min-height: 0; + max-height: 48px; +} +.content::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.32); +} + /* ── Track row ── */ .row { display: flex; From d747cba248c285e60d99988c639b1451b7a5ac80 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 15:50:21 +0100 Subject: [PATCH 08/29] style: resize handle highlight follows mouse as a centred 200px pill Replaces the full-height hover background with a ::after pseudo-element that tracks the cursor Y via a --mouse-y CSS custom property, so only a 200px pill centred on the mouse is lit up. Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/components/queue/QueueSidebar.tsx | 1 + renderer/src/styles/QueueSidebar.module.css | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/renderer/src/components/queue/QueueSidebar.tsx b/renderer/src/components/queue/QueueSidebar.tsx index 1929408..193bc03 100644 --- a/renderer/src/components/queue/QueueSidebar.tsx +++ b/renderer/src/components/queue/QueueSidebar.tsx @@ -283,6 +283,7 @@ export const QueueSidebar = forwardRef(function Queue
e.currentTarget.style.setProperty('--mouse-y', `${e.nativeEvent.offsetY}px`)} role="separator" aria-orientation="vertical" aria-label="Resize queue" diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index d63a77f..38b0e4d 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -85,7 +85,19 @@ touch-action: none; -webkit-app-region: no-drag; } -.resizeHandle:hover { background: rgba(255, 255, 255, 0.08); } +.resizeHandle::after { + content: ''; + position: absolute; + left: 1px; right: 1px; + height: 200px; + top: calc(var(--mouse-y, 50%) - 100px); + background: rgba(255, 255, 255, 0.18); + border-radius: 99px; + opacity: 0; + transition: opacity 0.15s, top 0s; + pointer-events: none; +} +.resizeHandle:hover::after { opacity: 1; } :global(html.resizingQueue), :global(html.resizingQueue) * { From 85e4848124704ac4e8d701691432784d780a54f1 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 15:51:32 +0100 Subject: [PATCH 09/29] style: resize handle glows and bulges with a purple lens on hover Replaces the flat pill with a 28px-wide radial gradient ellipse that bulges out from the 6px handle, blurred + box-shadowed in purple to create a soft glow. Docked sidebar set to overflow: visible so the lens can spill into the main content area. Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/QueueSidebar.module.css | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index 38b0e4d..87b05dd 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -47,6 +47,7 @@ background: transparent; transform: none; transition: none; + overflow: visible; /* Sit above the TopNav drag region (z-index 199) so the buttons below are clickable; the dockedTopBar provides its own drag region. */ z-index: 200; @@ -88,14 +89,22 @@ .resizeHandle::after { content: ''; position: absolute; - left: 1px; right: 1px; - height: 200px; - top: calc(var(--mouse-y, 50%) - 100px); - background: rgba(255, 255, 255, 0.18); + left: 50%; + transform: translateX(-50%); + width: 28px; + height: 220px; + top: calc(var(--mouse-y, 50%) - 110px); + background: radial-gradient(ellipse 50% 50% at 50% 50%, + rgba(200, 170, 255, 0.55) 0%, + rgba(160, 120, 255, 0.2) 45%, + transparent 70% + ); border-radius: 99px; opacity: 0; - transition: opacity 0.15s, top 0s; + transition: opacity 0.2s, top 0s; pointer-events: none; + filter: blur(2px); + box-shadow: 0 0 18px 6px rgba(160, 100, 255, 0.3); } .resizeHandle:hover::after { opacity: 1; } From 81797401219f024883435ffec8e0b78ffd6c91fd Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 15:52:09 +0100 Subject: [PATCH 10/29] style: remove outer box-shadow glow from resize handle Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/QueueSidebar.module.css | 1 - 1 file changed, 1 deletion(-) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index 87b05dd..a1f57ec 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -104,7 +104,6 @@ transition: opacity 0.2s, top 0s; pointer-events: none; filter: blur(2px); - box-shadow: 0 0 18px 6px rgba(160, 100, 255, 0.3); } .resizeHandle:hover::after { opacity: 1; } From 2cc386dc5693028ae0f21ec84b793d99752418ac Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 15:57:43 +0100 Subject: [PATCH 11/29] =?UTF-8?q?style:=20scrollbar=20=E2=80=94=20top=20ga?= =?UTF-8?q?p,=20shorter=20thumb,=20wider=20hit=20box?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/QueueSidebar.module.css | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index a1f57ec..cd6d9c2 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -192,19 +192,23 @@ } .content::-webkit-scrollbar { - width: 6px; + width: 16px; /* wide hit box */ } .content::-webkit-scrollbar-track { background: transparent; + margin-top: 20px; /* never touch top */ + margin-bottom: 8px; } .content::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.18); + /* transparent border shrinks visual width while keeping 16px hit box */ + border: 5px solid transparent; + background-clip: padding-box; + background-color: rgba(255, 255, 255, 0.18); border-radius: 99px; - min-height: 0; - max-height: 48px; + max-height: 32px; } .content::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.32); + background-color: rgba(255, 255, 255, 0.32); } /* ── Track row ── */ From 77ffe97ddbbecfc50ea51c8a555f8711aed17ce1 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 15:58:15 +0100 Subject: [PATCH 12/29] style: scrollbar thumb taller (52px) and thinner (4px) Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/QueueSidebar.module.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index cd6d9c2..010d25e 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -201,11 +201,11 @@ } .content::-webkit-scrollbar-thumb { /* transparent border shrinks visual width while keeping 16px hit box */ - border: 5px solid transparent; + border: 6px solid transparent; background-clip: padding-box; background-color: rgba(255, 255, 255, 0.18); border-radius: 99px; - max-height: 32px; + max-height: 52px; } .content::-webkit-scrollbar-thumb:hover { background-color: rgba(255, 255, 255, 0.32); From f4c1887197c83d1e31cb82a95945c73c30931bc2 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 15:59:23 +0100 Subject: [PATCH 13/29] style: scrollbar 6px wide, 10px hit box, 124px thumb Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/QueueSidebar.module.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index 010d25e..b433865 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -192,7 +192,7 @@ } .content::-webkit-scrollbar { - width: 16px; /* wide hit box */ + width: 10px; } .content::-webkit-scrollbar-track { background: transparent; @@ -201,11 +201,11 @@ } .content::-webkit-scrollbar-thumb { /* transparent border shrinks visual width while keeping 16px hit box */ - border: 6px solid transparent; + border: 2px solid transparent; background-clip: padding-box; background-color: rgba(255, 255, 255, 0.18); border-radius: 99px; - max-height: 52px; + max-height: 124px; } .content::-webkit-scrollbar-thumb:hover { background-color: rgba(255, 255, 255, 0.32); From 435255baef2d0aad660d65f70ae63835197d8562 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 15:59:59 +0100 Subject: [PATCH 14/29] style: scrollbar thumb capped at 20% of container height Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/QueueSidebar.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index b433865..770850f 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -205,7 +205,7 @@ background-clip: padding-box; background-color: rgba(255, 255, 255, 0.18); border-radius: 99px; - max-height: 124px; + max-height: 20%; } .content::-webkit-scrollbar-thumb:hover { background-color: rgba(255, 255, 255, 0.32); From 8190648cc85ac94ec6ac2c49478f77e685f145f0 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 16:01:45 +0100 Subject: [PATCH 15/29] style: scrollbar thumb max-height 300px Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/QueueSidebar.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index 770850f..750f299 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -205,7 +205,7 @@ background-clip: padding-box; background-color: rgba(255, 255, 255, 0.18); border-radius: 99px; - max-height: 20%; + max-height: 300px; } .content::-webkit-scrollbar-thumb:hover { background-color: rgba(255, 255, 255, 0.32); From d084ddeac339c30268ee47864729f8a52a1ea3cb Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 16:03:25 +0100 Subject: [PATCH 16/29] =?UTF-8?q?style:=20global=20scrollbar=20=E2=80=94?= =?UTF-8?q?=2010px=20hit=20box,=206px=20visual=20pill,=20top=20gap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/global.css | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/renderer/src/styles/global.css b/renderer/src/styles/global.css index 9a8de38..f2ec0c9 100644 --- a/renderer/src/styles/global.css +++ b/renderer/src/styles/global.css @@ -76,16 +76,20 @@ html[data-mini] #root { } ::-webkit-scrollbar { - width: 4px; - height: 4px; + width: 10px; + height: 10px; } ::-webkit-scrollbar-track { background: transparent; + margin-top: 20px; + margin-bottom: 8px; } ::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); - border-radius: 4px; + border: 2px solid transparent; + background-clip: padding-box; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 99px; } ::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.18); + background-color: rgba(255, 255, 255, 0.22); } From 6188bf5cc3f1ba063a6dcb5da71e9bd4f62d4541 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 16:05:04 +0100 Subject: [PATCH 17/29] feat: hide queue button in player bar and header when queue is docked Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/components/PlayerBar.tsx | 8 +++++--- renderer/src/components/TopNav.tsx | 16 +++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/renderer/src/components/PlayerBar.tsx b/renderer/src/components/PlayerBar.tsx index 3961cb7..c148bec 100644 --- a/renderer/src/components/PlayerBar.tsx +++ b/renderer/src/components/PlayerBar.tsx @@ -305,9 +305,11 @@ export function PlayerBar({ isAuthed, playback, onToggleQueue, onShuffle, queueM > - + {queueMode !== 'docked' && ( + + )} )} - + {queueMode !== 'docked' && ( + + )}
From 7f91be3544607ff48a74c3fbf78307a7f8e14577 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 16:07:13 +0100 Subject: [PATCH 18/29] feat: centre player bar over main content area when queue is docked Sets --docked-queue-w CSS variable on the shell when docked so the fixed player bar offsets its 50% anchor by the sidebar width. Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/App.tsx | 5 ++++- renderer/src/styles/PlayerBar.module.css | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx index 1387cae..675c3d1 100644 --- a/renderer/src/App.tsx +++ b/renderer/src/App.tsx @@ -436,7 +436,10 @@ function MainApp() { const splashReady = isAuthed && groups.length > 0 && !ytmLoading && !histLoading; return ( -
+
Date: Wed, 6 May 2026 16:08:13 +0100 Subject: [PATCH 19/29] feat: centre top nav pill over main content area when queue is docked Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/TopNav.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/src/styles/TopNav.module.css b/renderer/src/styles/TopNav.module.css index 0d9b4e2..1614dff 100644 --- a/renderer/src/styles/TopNav.module.css +++ b/renderer/src/styles/TopNav.module.css @@ -13,7 +13,7 @@ .navRoot { position: fixed; top: 12px; - left: 50%; + left: calc((100vw - var(--docked-queue-w, 0px)) / 2); transform: translateX(-50%); z-index: 200; display: flex; From 3db71eac93fab18c376a4f62499e8f44836bad40 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 16:12:51 +0100 Subject: [PATCH 20/29] feat: player bar and nav pill recentre live during queue resize drag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds onResizeWidthLive prop to QueueSidebar — fires on every pointer move during resize. App.tsx handles it by mutating the shell's --docked-queue-w CSS variable directly via a ref, avoiding any React re-render on the hot drag path. Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/App.tsx | 6 ++++++ renderer/src/components/queue/QueueSidebar.tsx | 3 +++ 2 files changed, 9 insertions(+) diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx index 675c3d1..dc86fc9 100644 --- a/renderer/src/App.tsx +++ b/renderer/src/App.tsx @@ -89,6 +89,10 @@ function MainApp() { const [queueMode, setQueueMode] = useState<'floating' | 'docked'>('floating'); const [queueDockedWidth, setQueueDockedWidth] = useState(380); const queueSidebarRef = useRef(null); + const shellRef = useRef(null); + const handleResizeWidthLive = useCallback((width: number) => { + shellRef.current?.style.setProperty('--docked-queue-w', `${width}px`); + }, []); useEffect(() => { window.sonos.getDisplayName().then(setDisplayName); @@ -437,6 +441,7 @@ function MainApp() { return (
@@ -513,6 +518,7 @@ function MainApp() { onAddToQueue={handleAddToQueue} dockedWidth={queueDockedWidth} onResizeWidth={handleSetQueueDockedWidth} + onResizeWidthLive={handleResizeWidthLive} /> )}
diff --git a/renderer/src/components/queue/QueueSidebar.tsx b/renderer/src/components/queue/QueueSidebar.tsx index 193bc03..8ea0ebb 100644 --- a/renderer/src/components/queue/QueueSidebar.tsx +++ b/renderer/src/components/queue/QueueSidebar.tsx @@ -29,6 +29,7 @@ interface Props { onAddToQueue: (item: SonosItem, position: number) => void; dockedWidth?: number; onResizeWidth?: (width: number) => void; + onResizeWidthLive?: (width: number) => void; } export interface QueueSidebarHandle { @@ -58,6 +59,7 @@ export const QueueSidebar = forwardRef(function Queue onAddToQueue, dockedWidth, onResizeWidth, + onResizeWidthLive, }, ref ) { @@ -165,6 +167,7 @@ export const QueueSidebar = forwardRef(function Queue 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'); From f4a081fd00774b85a9c11d288376f937a4fe698b Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 16:14:29 +0100 Subject: [PATCH 21/29] feat: queue max width derived from player bar width + padding Removes the hardcoded MAX_DOCKED_WIDTH=700 / MIN_ROUTES_WIDTH=320. Max queue width is now window.innerWidth - (800 + 64), guaranteeing the main content area is always at least the player bar's inner width (800px) plus 32px breathing room on each side. Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/App.tsx | 2 +- renderer/src/components/queue/QueueSidebar.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx index dc86fc9..111607a 100644 --- a/renderer/src/App.tsx +++ b/renderer/src/App.tsx @@ -103,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); diff --git a/renderer/src/components/queue/QueueSidebar.tsx b/renderer/src/components/queue/QueueSidebar.tsx index 8ea0ebb..351559d 100644 --- a/renderer/src/components/queue/QueueSidebar.tsx +++ b/renderer/src/components/queue/QueueSidebar.tsx @@ -37,8 +37,8 @@ export interface QueueSidebarHandle { } 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(function QueueSidebar( { @@ -162,7 +162,7 @@ export const QueueSidebar = forwardRef(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))); From 79e401b72c9b19e64d79525f8b25a8fd6e00b057 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 16:16:46 +0100 Subject: [PATCH 22/29] feat: enforce minimum window width of 1144px (864 + min queue width) Prevents the window being resized narrow enough to crunch the docked queue: minWidth = 864px (player bar 800px + 32px each side) + 280px (MIN_DOCKED_WIDTH). Also bumps default size from 960x640 to 1280x720. Co-Authored-By: Claude Sonnet 4.6 --- src/main.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main.ts b/src/main.ts index 5acf7ea..70c04c3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1078,8 +1078,10 @@ function onAuthReady(): void { function createUIWindow(): void { uiWin = new BrowserWindow({ - width: 960, - height: 640, + width: 1280, + height: 720, + minWidth: 864 + 280, // player bar (800) + 32px each side + min queue width + minHeight: 480, title: `True-Tunes v${app.getVersion()}`, backgroundColor: '#1c1c1e', frame: false, From 4cc913b77b501a976ffdb5b97fd7f13c4f8e7372 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 16:21:33 +0100 Subject: [PATCH 23/29] style: resize handle glow emanates right only into queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anchors ::after to left:0 and extends 120px rightward. Radial gradient centred at the left edge fades into the queue — nothing bleeds left. Removed filter:blur which was the source of leftward spill. Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/QueueSidebar.module.css | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index 750f299..8f4182d 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -89,21 +89,18 @@ .resizeHandle::after { content: ''; position: absolute; - left: 50%; - transform: translateX(-50%); - width: 28px; + left: 0; + width: 120px; height: 220px; top: calc(var(--mouse-y, 50%) - 110px); - background: radial-gradient(ellipse 50% 50% at 50% 50%, - rgba(200, 170, 255, 0.55) 0%, - rgba(160, 120, 255, 0.2) 45%, + background: radial-gradient(ellipse 100% 50% at 0% 50%, + rgba(200, 160, 255, 0.55) 0%, + rgba(160, 110, 255, 0.25) 45%, transparent 70% ); - border-radius: 99px; opacity: 0; transition: opacity 0.2s, top 0s; pointer-events: none; - filter: blur(2px); } .resizeHandle:hover::after { opacity: 1; } From 140df3130a6906c123944f3066b163e1ae475dcf Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 16:22:15 +0100 Subject: [PATCH 24/29] =?UTF-8?q?style:=20resize=20handle=20glow=20?= =?UTF-8?q?=E2=80=94=20larger=20(280x400)=20and=20more=20diffuse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/styles/QueueSidebar.module.css | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index 8f4182d..d156db4 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -90,13 +90,13 @@ content: ''; position: absolute; left: 0; - width: 120px; - height: 220px; - top: calc(var(--mouse-y, 50%) - 110px); + width: 280px; + height: 400px; + top: calc(var(--mouse-y, 50%) - 200px); background: radial-gradient(ellipse 100% 50% at 0% 50%, - rgba(200, 160, 255, 0.55) 0%, - rgba(160, 110, 255, 0.25) 45%, - transparent 70% + rgba(200, 160, 255, 0.18) 0%, + rgba(160, 110, 255, 0.08) 50%, + transparent 80% ); opacity: 0; transition: opacity 0.2s, top 0s; From 4eb0b7e044b2cfd785bf91dd307af5d3da9de475 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 16:29:20 +0100 Subject: [PATCH 25/29] style: Refine QueueSidebar resize handle appearance and usability --- renderer/src/styles/QueueSidebar.module.css | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index d156db4..7fa896f 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -79,7 +79,7 @@ .resizeHandle { position: absolute; top: 0; left: 0; bottom: 0; - width: 6px; + width: 16px; cursor: ew-resize; background: transparent; z-index: 2; @@ -90,16 +90,17 @@ content: ''; position: absolute; left: 0; + top: 0; + bottom: 0; width: 280px; - height: 400px; - top: calc(var(--mouse-y, 50%) - 200px); - background: radial-gradient(ellipse 100% 50% at 0% 50%, + background: radial-gradient( + ellipse 100% 300px at 0px var(--mouse-y, 50%), rgba(200, 160, 255, 0.18) 0%, rgba(160, 110, 255, 0.08) 50%, transparent 80% ); opacity: 0; - transition: opacity 0.2s, top 0s; + transition: opacity 0.2s; pointer-events: none; } .resizeHandle:hover::after { opacity: 1; } From a6fb04100f46ff771c2ba9367185f8d7f7d7bf60 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 17:07:02 +0100 Subject: [PATCH 26/29] feat: scroll-sync panel-colour gradient and docked queue visual polish - Gradient in docked queue fades out as main content is scrolled down, using a capture-phase scroll listener on the routes container - Resets to full opacity on route navigation - Fix TS error: void the removeProperty cleanup return in useDominantColor - Row hover glow via ::before radial gradient tracking mouse position - Resize handle glow pulses into queue only (no leftward bleed) - winPill wrapper around window controls in docked mode - Header reordered as footer when docked - subAlbum hit-box fix: block + fit-content instead of inline-block - Scrollbar polish: bigger hit box, pill thumb, never touches top - body::after gradient opacity driven by --panel-gradient-opacity CSS var Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/App.tsx | 20 ++++++++++++ .../components/queue/DraggableQueueRow.tsx | 5 +++ renderer/src/hooks/useDominantColor.ts | 9 ++++++ renderer/src/styles/App.module.css | 20 ++++++++++++ renderer/src/styles/QueueSidebar.module.css | 31 +++++++++++++++++-- renderer/src/styles/global.css | 13 ++++---- 6 files changed, 90 insertions(+), 8 deletions(-) diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx index 111607a..96ccdf9 100644 --- a/renderer/src/App.tsx +++ b/renderer/src/App.tsx @@ -90,10 +90,28 @@ function MainApp() { const [queueDockedWidth, setQueueDockedWidth] = useState(380); const queueSidebarRef = useRef(null); const shellRef = useRef(null); + const routesRef = useRef(null); const handleResizeWidthLive = useCallback((width: number) => { shellRef.current?.style.setProperty('--docked-queue-w', `${width}px`); }, []); + // Fade the queue panel-colour gradient when the main content is scrolled down + const location = useLocation(); + useEffect(() => { + shellRef.current?.style.setProperty('--panel-gradient-opacity', '1'); + }, [location.pathname]); + useEffect(() => { + const el = routesRef.current; + if (!el) return; + function onScroll(e: Event) { + const target = e.target as HTMLElement; + const opacity = Math.max(0, 1 - target.scrollTop / 250); + shellRef.current?.style.setProperty('--panel-gradient-opacity', String(opacity)); + } + el.addEventListener('scroll', onScroll, true); + return () => el.removeEventListener('scroll', onScroll, true); + }, []); + useEffect(() => { window.sonos.getDisplayName().then(setDisplayName); window.sonos.getQueueMode().then(setQueueMode).catch(() => {}); @@ -464,6 +482,7 @@ function MainApp() { onSetQueueMode={handleSetQueueMode} />
+
} /> } /> +
{queueMode === 'docked' && ( { + 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)} diff --git a/renderer/src/hooks/useDominantColor.ts b/renderer/src/hooks/useDominantColor.ts index 5b1785c..e78ca27 100644 --- a/renderer/src/hooks/useDominantColor.ts +++ b/renderer/src/hooks/useDominantColor.ts @@ -38,5 +38,14 @@ export function useDominantColor(src: string | null): string | null { img.src = src; }, [src]); + useEffect(() => { + if (color) { + document.documentElement.style.setProperty('--panel-color', color); + } else { + document.documentElement.style.removeProperty('--panel-color'); + } + return () => { document.documentElement.style.removeProperty('--panel-color'); }; + }, [color]); + return color; } diff --git a/renderer/src/styles/App.module.css b/renderer/src/styles/App.module.css index b19590a..20ea6d6 100644 --- a/renderer/src/styles/App.module.css +++ b/renderer/src/styles/App.module.css @@ -15,6 +15,26 @@ min-height: 0; overflow: hidden; display: flex; + position: relative; +} + +.body::after { + content: ''; + position: absolute; + top: 0; + right: 0; + width: var(--docked-queue-w, 0px); + height: 100%; + 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% + ); + pointer-events: none; + opacity: var(--panel-gradient-opacity, 1); + transition: background 0.8s ease, opacity 0.3s ease; + z-index: 1; } .toast { diff --git a/renderer/src/styles/QueueSidebar.module.css b/renderer/src/styles/QueueSidebar.module.css index 7fa896f..1b7f72a 100644 --- a/renderer/src/styles/QueueSidebar.module.css +++ b/renderer/src/styles/QueueSidebar.module.css @@ -121,6 +121,14 @@ flex-shrink: 0; } +.sidebar.docked .header { + order: 99; + border-bottom: none; + border-top: 1px solid var(--border); + padding: 12px 16px; +} + + .title { font-size: 14px; font-weight: 600; @@ -218,8 +226,25 @@ border-radius: var(--r-sm); transition: background 0.1s; cursor: default; + position: relative; + overflow: hidden; +} +.row::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient( + ellipse 60% 120% at var(--mx, 50%) var(--my, 50%), + rgba(190, 150, 255, 0.13) 0%, + rgba(150, 100, 255, 0.05) 50%, + transparent 75% + ); + opacity: 0; + transition: opacity 0.2s; + pointer-events: none; } -.row:hover { background: var(--bg-2); } +.row:hover::before { opacity: 1; } +.row:hover { background: rgba(255, 255, 255, 0.03); } .row.selected { background: rgba(255,255,255,0.1); } .row.selected:hover { background: rgba(255,255,255,0.13); } .row.playing .name { color: #fff; font-weight: 600; } @@ -289,11 +314,13 @@ .subAlbum { display: block; + width: fit-content; + max-width: 100%; background: none; border: none; padding: 0; font-size: 11px; font-family: inherit; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - width: 100%; text-align: left; + text-align: left; cursor: pointer; transition: color 0.12s; } .subAlbum:hover { color: var(--text); text-decoration: underline; } diff --git a/renderer/src/styles/global.css b/renderer/src/styles/global.css index f2ec0c9..faf5f3c 100644 --- a/renderer/src/styles/global.css +++ b/renderer/src/styles/global.css @@ -26,25 +26,25 @@ user-select: none; } -input[type="text"], +input[type='text'], input:not([type]), textarea { user-select: text; } @font-face { - font-family: "San Francisco"; + font-family: 'San Francisco'; font-weight: 400; - src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-regular-webfont.woff"); + src: url('https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-regular-webfont.woff'); } body { font-family: - "San Francisco", + 'San Francisco', -apple-system, BlinkMacSystemFont, - "SF Pro Text", - "Segoe UI", + 'SF Pro Text', + 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); @@ -78,6 +78,7 @@ html[data-mini] #root { ::-webkit-scrollbar { width: 10px; height: 10px; + background: transparent; } ::-webkit-scrollbar-track { background: transparent; From 70e400a253a4f576b489ed7ae0cbd4d0f237949e Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 17:14:49 +0100 Subject: [PATCH 27/29] fix: restore main content scroll broken by routes wrapper div Use document-level capture scroll listener instead of a wrapper div around Routes. The wrapper div disrupted the flex layout that panels rely on for height. Queue scroll is excluded via data-queue-content attr. Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/App.tsx | 11 ++++------- renderer/src/components/queue/QueueSidebar.tsx | 1 + 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx index 96ccdf9..2b9d36b 100644 --- a/renderer/src/App.tsx +++ b/renderer/src/App.tsx @@ -90,7 +90,6 @@ function MainApp() { const [queueDockedWidth, setQueueDockedWidth] = useState(380); const queueSidebarRef = useRef(null); const shellRef = useRef(null); - const routesRef = useRef(null); const handleResizeWidthLive = useCallback((width: number) => { shellRef.current?.style.setProperty('--docked-queue-w', `${width}px`); }, []); @@ -101,15 +100,15 @@ function MainApp() { shellRef.current?.style.setProperty('--panel-gradient-opacity', '1'); }, [location.pathname]); useEffect(() => { - const el = routesRef.current; - if (!el) return; function onScroll(e: Event) { const target = e.target as HTMLElement; + // Ignore queue sidebar's own scroll container + if (target.dataset.queueContent) return; const opacity = Math.max(0, 1 - target.scrollTop / 250); shellRef.current?.style.setProperty('--panel-gradient-opacity', String(opacity)); } - el.addEventListener('scroll', onScroll, true); - return () => el.removeEventListener('scroll', onScroll, true); + document.addEventListener('scroll', onScroll, true); + return () => document.removeEventListener('scroll', onScroll, true); }, []); useEffect(() => { @@ -482,7 +481,6 @@ function MainApp() { onSetQueueMode={handleSetQueueMode} />
-
} /> } /> -
{queueMode === 'docked' && ( (function Queue
{ e.preventDefault(); From 50d3479d4a56346f251c946a850a39923f4a90e4 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 17:16:12 +0100 Subject: [PATCH 28/29] =?UTF-8?q?fix:=20simplify=20gradient=20=E2=80=94=20?= =?UTF-8?q?set=20directly=20on=20.body,=20no=20pseudo-element=20or=20JS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/App.tsx | 19 +------------------ .../src/components/queue/QueueSidebar.tsx | 3 +-- renderer/src/styles/App.module.css | 14 +------------- 3 files changed, 3 insertions(+), 33 deletions(-) diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx index 2b9d36b..8bc2319 100644 --- a/renderer/src/App.tsx +++ b/renderer/src/App.tsx @@ -94,24 +94,7 @@ function MainApp() { shellRef.current?.style.setProperty('--docked-queue-w', `${width}px`); }, []); - // Fade the queue panel-colour gradient when the main content is scrolled down - const location = useLocation(); - useEffect(() => { - shellRef.current?.style.setProperty('--panel-gradient-opacity', '1'); - }, [location.pathname]); - useEffect(() => { - function onScroll(e: Event) { - const target = e.target as HTMLElement; - // Ignore queue sidebar's own scroll container - if (target.dataset.queueContent) return; - const opacity = Math.max(0, 1 - target.scrollTop / 250); - shellRef.current?.style.setProperty('--panel-gradient-opacity', String(opacity)); - } - document.addEventListener('scroll', onScroll, true); - return () => document.removeEventListener('scroll', onScroll, true); - }, []); - - useEffect(() => { +useEffect(() => { window.sonos.getDisplayName().then(setDisplayName); window.sonos.getQueueMode().then(setQueueMode).catch(() => {}); window.sonos.getQueueDockedWidth().then(setQueueDockedWidth).catch(() => {}); diff --git a/renderer/src/components/queue/QueueSidebar.tsx b/renderer/src/components/queue/QueueSidebar.tsx index e3c58b8..aa30736 100644 --- a/renderer/src/components/queue/QueueSidebar.tsx +++ b/renderer/src/components/queue/QueueSidebar.tsx @@ -331,8 +331,7 @@ export const QueueSidebar = forwardRef(function Queue
{ e.preventDefault(); e.dataTransfer.dropEffect = e.dataTransfer.types.includes('application/sonos-item-list') ? 'copy' : 'move'; diff --git a/renderer/src/styles/App.module.css b/renderer/src/styles/App.module.css index 20ea6d6..1d48ed1 100644 --- a/renderer/src/styles/App.module.css +++ b/renderer/src/styles/App.module.css @@ -16,25 +16,13 @@ overflow: hidden; display: flex; position: relative; -} - -.body::after { - content: ''; - position: absolute; - top: 0; - right: 0; - width: var(--docked-queue-w, 0px); - height: 100%; 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% ); - pointer-events: none; - opacity: var(--panel-gradient-opacity, 1); - transition: background 0.8s ease, opacity 0.3s ease; - z-index: 1; + transition: background 0.8s ease; } .toast { From 8f0de968e01e08107d0085d4997487658a7be784 Mon Sep 17 00:00:00 2001 From: Joe Pitts Date: Wed, 6 May 2026 17:18:42 +0100 Subject: [PATCH 29/29] fix: prevent song change from clobbering body gradient colour useDominantColor now only sets --panel-color globally when setGlobal:true. Page components (ArtistHero, ArtistPanel, AlbumPanel) opt in; useNowPlaying does not, so track changes no longer overwrite the gradient. Co-Authored-By: Claude Sonnet 4.6 --- renderer/src/components/album/AlbumPanel.tsx | 2 +- renderer/src/components/artist/ArtistHero.tsx | 2 +- renderer/src/components/artist/ArtistPanel.tsx | 2 +- renderer/src/hooks/useDominantColor.ts | 5 +++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/renderer/src/components/album/AlbumPanel.tsx b/renderer/src/components/album/AlbumPanel.tsx index 0aa5f28..b1b0812 100644 --- a/renderer/src/components/album/AlbumPanel.tsx +++ b/renderer/src/components/album/AlbumPanel.tsx @@ -52,7 +52,7 @@ export function AlbumPanel({ onAddToQueue, queueOpen }: Props) { const artist = data?.artist ?? ((item as Record)?.['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()); diff --git a/renderer/src/components/artist/ArtistHero.tsx b/renderer/src/components/artist/ArtistHero.tsx index ea43974..7bc36af 100644 --- a/renderer/src/components/artist/ArtistHero.tsx +++ b/renderer/src/components/artist/ArtistHero.tsx @@ -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>(new Set()); diff --git a/renderer/src/components/artist/ArtistPanel.tsx b/renderer/src/components/artist/ArtistPanel.tsx index aed8a8c..eeefbd2 100644 --- a/renderer/src/components/artist/ArtistPanel.tsx +++ b/renderer/src/components/artist/ArtistPanel.tsx @@ -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; diff --git a/renderer/src/hooks/useDominantColor.ts b/renderer/src/hooks/useDominantColor.ts index e78ca27..16c7931 100644 --- a/renderer/src/hooks/useDominantColor.ts +++ b/renderer/src/hooks/useDominantColor.ts @@ -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(null); useEffect(() => { @@ -39,13 +39,14 @@ export function useDominantColor(src: string | null): string | null { }, [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]); + }, [color, setGlobal]); return color; }