openvibers/openvibe.live Commits

Channel page: the sticky section bar follows the navbar, edge to edge (#690)

OP OpenVibers committed 5 days ago · 2f2822ff6b · 2 files, +37 −0

The owner's phone screenshot: when the bar stuck, a strip of page showed above it (it stuck at the navbar's height
after the navbar had slid away) and down both sides (it sat inside the page's gutter). Now it rises to the top of the
screen when the navbar hides, with the navbar's own easing, and comes back under it when the navbar returns; it runs
edge to edge, frosted; once stuck (app-channel.js sets .is-stuck, one check a frame) it lifts with a soft shadow and
an accent hairline. Reduced motion: no transition.

Changes

ddiff --git a/public/css/features/channel.css b/public/css/features/channel.css
iindex de4c03aa..ec1a0487 100644
---- a/public/css/features/channel.css
++++ b/public/css/features/channel.css
@@@ -1742,6 +1742,31 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
folds. Scoped to the channel: the dashboard's settings tabs keep their tabs. */
#page-channel .channel-below-fold > .container { display: flex; flex-direction: column; }
#page-channel #ch-tabs { order: 0; position: sticky; top: var(--ovnav-h, 56px); z-index: 6; margin: 0 0 16px; background: var(--bg-primary); }
++/* The sticky section bar (owner 11-08: "the empty space gap above these when they are fixed like this is ugly"):
++ it follows the navbar. While the bar is there it sits under it; when a scroll down slides the navbar away it rises to
++ the top of the screen with the navbar's own easing, so no strip of page shows above it. It runs edge to edge (no
++ page showing down the sides), is frosted, and once it is stuck (app-channel.js sets .is-stuck) it lifts with a soft
++ shadow and an accent hairline. */
++#page-channel #ch-tabs {
++ top: var(--ovnav-h, 56px);
++ transition: top .45s cubic-bezier(.2, .7, .2, 1), box-shadow .25s ease, border-color .25s ease;
++ margin-inline: calc(-1 * clamp(16px, 2vw, 28px));
++ padding-inline: clamp(16px, 2vw, 28px);
++ background: color-mix(in srgb, var(--bg-primary) 88%, transparent);
++ -webkit-backdrop-filter: saturate(1.4) blur(14px);
++ backdrop-filter: saturate(1.4) blur(14px);
++}
++html:has(.openvibe-navbar.ovnav-hide) #page-channel #ch-tabs { top: 0; padding-top: env(safe-area-inset-top, 0px); }
++#page-channel #ch-tabs.is-stuck {
++ border-bottom-color: color-mix(in srgb, var(--accent) 28%, var(--border));
++ box-shadow: 0 12px 20px -16px rgba(0, 0, 0, .7);
++}
++@media (max-width: 768px) {
++ /* The phone's gutter is the container's 16 px plus the fold's own 8 px. */
++ #page-channel #ch-tabs { margin-inline: -24px; padding-inline: 24px; }
++}
++@media (prefers-reduced-motion: reduce) { #page-channel #ch-tabs { transition: none; } }
++html[data-ov-motion="reduced"] #page-channel #ch-tabs { transition: none; }
#page-channel .ch-tab-panel { display: block; order: 50; margin: 0 0 30px; animation: none; scroll-margin-top: calc(var(--ovnav-h, 56px) + 56px); }
#page-channel .ch-tab-panel[hidden] { display: none; }
#page-channel #ch-panel-about { order: 1; } #page-channel #ch-panel-videos { order: 2; } #page-channel #ch-panel-clips { order: 3; }
ddiff --git a/public/js/app-channel.js b/public/js/app-channel.js
iindex 7be10c5f..221352cd 100644
---- a/public/js/app-channel.js
++++ b/public/js/app-channel.js
@@@ -670,6 +670,18 @@ function _watchTabsClip() {
t.addEventListener('scroll', upd, { passive: true });
if ('ResizeObserver' in window) new ResizeObserver(upd).observe(t); else window.addEventListener('resize', upd);
upd();
++ // Stuck to the top (channel.css lifts it with a shadow then): its box sits at its own sticky offset. One check a
++ // frame while the page scrolls; the listener leaves with the bar when the channel is swapped out.
++ let raf = 0;
++ const stuck = () => {
++ raf = 0;
++ if (!t.isConnected) { window.removeEventListener('scroll', onScroll); return; }
++ const top = parseFloat(getComputedStyle(t).top) || 0;
++ t.classList.toggle('is-stuck', window.scrollY > 0 && t.getBoundingClientRect().top <= top + 1);
++ };
++ const onScroll = () => { if (!raf) raf = requestAnimationFrame(stuck); };
++ window.addEventListener('scroll', onScroll, { passive: true });
++ stuck();
}
// A direct visit paints the channel's markup before its data. Until the data (ch-data) channel.css keeps the offline