feat(ux): card unification, slim icon nav, filter/plex fixes (7-item batch)

1A ChannelPage "Full history" button no longer wraps (whitespace-nowrap).
1B Extract the manager's press-and-hold priority stepper into a shared
   PriorityStepper and add it to the channel detail header (subscribed, non-demo);
   backend channel_detail now returns the per-subscription `priority`.
2  Unify YouTube + Plex media cards onto one look/behaviour: drop the whole-card
   hover LIFT (VideoCard) — which also fixes the clipped-top bug — and instead the
   inner thumbnail/poster ZOOMS on hover (both); give the flat Plex posters the same
   glass-card 3D depth + accent hover border as the feed cards (shadow-md is a
   transparent no-op here, so it's glass-card that provides the depth); clamp Plex
   card titles to a fixed 2-line height so 1- vs 2-line titles align.
3  Nav rail is now always a slim icon-only column, flush to the left edge; each icon
   reveals a floating name label on hover (Tooltip gains `always` + `openDelay`, and
   a right side). Removed the pin/unpin toggle, the expand-on-hover mode and the
   navCollapsed pref plumbing.
4  Scope the side-panel peek-open HOVER to the tab handle + the card (not the whole
   full-height slot next to the rail), so approaching the nav no longer pops the
   filter open; focus stays container-wide. useHoverFocusWithin gains a hover-out
   grace period + split hover/focus handler groups.
5  Plex art backdrop fills the whole viewport (a fixed inset-0 layer behind the
   transparent app) instead of only the inset <main> box.
6  Plex season binge continues across seasons: the season view's play queue now
   spans the whole show (allKeys), so a season's last episode rolls into the next
   season's first (seasonKeys stays for "add season to playlist").
7  When a Plex video/episode ends with nothing queued next, the player returns to
   the previous page (onEnded → onClose) instead of stranding a black screen.
This commit is contained in:
2026-08-11 02:20:52 +02:00
parent e5d5735b9a
commit 9d7a2a75d8
14 changed files with 376 additions and 348 deletions
+17 -24
View File
@@ -77,32 +77,25 @@ export function Filterable({
);
}
// Paint the item's art as a faint FIXED backdrop on the page's <main> scroll container (HTPC-style),
// so the glass panels float over it. Cleared on unmount / when disabled / when there's no art.
// Paint the item's art as a faint FULL-VIEWPORT backdrop (HTPC-style) so the glass chrome floats over
// it. A dedicated `position:fixed inset-0` layer at z-index -1 sits BEHIND the (transparent / glassy)
// app content — so the art fills the whole page edge-to-edge, under the nav rail and floating header
// too, instead of only the <main> box (which is inset by the rail, the header and the scrollbar
// gutter). Removed on unmount / when disabled / when there's no art.
export function useArtBackdrop(art: string | null | undefined, enabled: boolean) {
useLayoutEffect(() => {
const main = document.querySelector("main");
if (!main) return;
const s = main.style;
const clear = () => {
s.backgroundImage = "";
s.backgroundSize = "";
s.backgroundPosition = "";
s.backgroundRepeat = "";
s.backgroundAttachment = "";
};
if (enabled && art) {
s.backgroundImage =
`linear-gradient(color-mix(in srgb, var(--bg) 60%, transparent), ` +
`color-mix(in srgb, var(--bg) 64%, transparent)), url("${art}")`;
s.backgroundSize = "cover";
s.backgroundPosition = "center 20%";
s.backgroundRepeat = "no-repeat";
s.backgroundAttachment = "fixed";
} else {
clear();
}
return clear;
if (!enabled || !art) return;
const el = document.createElement("div");
el.setAttribute("data-plex-art-backdrop", "");
el.style.cssText =
"position:fixed;inset:0;z-index:-1;pointer-events:none;" +
"background-size:cover;background-position:center 20%;background-repeat:no-repeat;";
// The faint --bg overlay keeps it subtle and text-safe (same tuning the old <main> paint used).
el.style.backgroundImage =
`linear-gradient(color-mix(in srgb, var(--bg) 60%, transparent), ` +
`color-mix(in srgb, var(--bg) 64%, transparent)), url("${art}")`;
document.body.appendChild(el);
return () => el.remove();
}, [art, enabled]);
}