feat(shell): sticky tabs+toolbar on the channel page
Generalise the fixed-chrome pattern to the channel detail page: the banner + identity now scroll away, while the tab row and the channel feed's toolbar stick to the top as a sticky sub-header — only the video grid scrolls. Mechanism: PageToolbarSlot (the toolbar portal target) is now an exported, overridable context. ChannelPage provides its own sticky sub-header node as the target, so the channelScoped Feed portals its toolbar up next to the tabs instead of into the shell's top band. Feed always portals its toolbar now (the slot decides where it lands). Also: the page scroller now fades only its BOTTOM edge — the top is bounded by the fixed/sticky chrome, so a top fade only bit into it. E2E: adds a lock that the channel tabs/toolbar stay pinned while the banner scrolls away; suite 15/15 on :5173, tsc + knip clean.
This commit is contained in:
@@ -7,10 +7,17 @@ import { useEffect, useState, type CSSProperties } from "react";
|
||||
// to attach to the scroller and the mask `style` to spread onto it. Uses a node-state callback
|
||||
// ref (not useRef) so the effect (re)runs whenever the element attaches — e.g. a SidePanel that
|
||||
// mounts collapsed and only renders its scroll body once expanded still wires up correctly.
|
||||
export function useScrollFade(fadePx = 20): {
|
||||
// `edges` gates which ends fade. The page scroller passes `{ top: false }`: its top is delimited by
|
||||
// the fixed/sticky module chrome (header, toolbar band, a sticky sub-header), so a top fade there
|
||||
// would only bite into that chrome instead of hinting at more content.
|
||||
export function useScrollFade(
|
||||
fadePx = 20,
|
||||
edges: { top?: boolean; bottom?: boolean } = {},
|
||||
): {
|
||||
ref: (node: HTMLElement | null) => void;
|
||||
style: CSSProperties;
|
||||
} {
|
||||
const { top: fadeTop = true, bottom: fadeBottom = true } = edges;
|
||||
const [el, setEl] = useState<HTMLElement | null>(null);
|
||||
const [fade, setFade] = useState({ up: false, down: false });
|
||||
|
||||
@@ -53,8 +60,8 @@ export function useScrollFade(fadePx = 20): {
|
||||
};
|
||||
}, [el]);
|
||||
|
||||
const mask = `linear-gradient(to bottom, transparent, #000 ${fade.up ? fadePx : 0}px, #000 calc(100% - ${
|
||||
fade.down ? fadePx : 0
|
||||
}px), transparent)`;
|
||||
const topPx = fadeTop && fade.up ? fadePx : 0;
|
||||
const bottomPx = fadeBottom && fade.down ? fadePx : 0;
|
||||
const mask = `linear-gradient(to bottom, transparent, #000 ${topPx}px, #000 calc(100% - ${bottomPx}px), transparent)`;
|
||||
return { ref: setEl, style: { maskImage: mask, WebkitMaskImage: mask } };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user