fix(ux): dock filter tab flush to the nav + reliable full-viewport plex backdrop

Follow-ups on the 7-item batch:
- The slim nav slot was still w-20 while its card is w-14, leaving a ~24px dead
  gap the filter tab/bar couldn't dock against. Slot is now w-14 (exactly the
  card width), so the filter's flat-left tab sits flush on the rail's right edge
  (measured gap 0), both collapsed and expanded.
- The Plex art backdrop was invisible: a position:fixed layer at z-index -1 is
  painted BEHIND the body's opaque background here, so nothing showed. Paint the
  art on the app root (#root) instead — it's exactly the viewport and a real
  ancestor of the transparent nav/header/content, so the art fills the whole page
  edge-to-edge and the glass chrome floats over it (verified in-browser).
This commit is contained in:
2026-08-11 02:53:55 +02:00
parent 9d7a2a75d8
commit 71bbf35f5a
2 changed files with 31 additions and 17 deletions
+2 -1
View File
@@ -201,7 +201,8 @@ export default function NavSidebar({
// Layout slot: reserves only the slim footprint when unpinned, so the expanded rail (below)
// overlays content instead of pushing it. Pinned reserves the full width (in-flow).
// Slim icon rail, flush against the left viewport edge (only top/bottom margin), never grows.
<div className="relative shrink-0 w-20">
// The slot is exactly the card's width so the filter tab/bar docks flush against its right edge.
<div className="relative shrink-0 w-14">
<nav
className="glass absolute top-0 bottom-0 left-0 z-rail rounded-2xl flex flex-col py-3 my-3 w-14 px-2"
aria-label={t("nav.primary")}
+29 -16
View File
@@ -78,24 +78,37 @@ export function Filterable({
}
// 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.
// it. Painted on the app root (#root) — which is exactly the viewport and sits BEHIND the transparent
// nav / header / content — so the art fills the whole page edge-to-edge (under the rail + floating
// header too), not just the inset <main> box. (A `position:fixed` layer at negative z-index does NOT
// work here: the body's opaque background is painted over it, so it's invisible; #root's background,
// being a real ancestor of the content, is the reliable full-viewport layer.) `attachment:fixed`
// keeps it viewport-anchored while the content scrolls. Cleared on unmount / disable / no art.
export function useArtBackdrop(art: string | null | undefined, enabled: boolean) {
useLayoutEffect(() => {
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();
const el = document.getElementById("root");
if (!el) return;
const s = el.style;
const clear = () => {
s.backgroundImage = "";
s.backgroundSize = "";
s.backgroundPosition = "";
s.backgroundRepeat = "";
s.backgroundAttachment = "";
};
if (enabled && art) {
// The faint --bg overlay keeps it subtle and text-safe (same tuning the old paint used).
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;
}, [art, enabled]);
}