From 0805e077a19d8b2261fdafbd9c8c82afd3d95263 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 9 Dec 2025 08:18:27 +0200 Subject: [PATCH] feat(ribbon): basic implementation for scroll pinning --- .../src/widgets/containers/content_header.css | 18 +++++++++ .../src/widgets/containers/content_header.ts | 39 +++++++++++++++++-- apps/client/src/widgets/ribbon/style.css | 12 +++++- 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 apps/client/src/widgets/containers/content_header.css diff --git a/apps/client/src/widgets/containers/content_header.css b/apps/client/src/widgets/containers/content_header.css new file mode 100644 index 000000000..322c88da9 --- /dev/null +++ b/apps/client/src/widgets/containers/content_header.css @@ -0,0 +1,18 @@ +.content-header-widget { + position: relative; + transition: position 0.3s ease, box-shadow 0.3s ease, z-index 0.3s ease; + z-index: 8; +} + +.content-header-widget.floating { + position: sticky; + top: 0; + z-index: 11; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background-color: var(--main-background-color, #fff); +} + +/* Ensure content inside doesn't get affected by the floating state */ +.content-header-widget > * { + transition: inherit; +} diff --git a/apps/client/src/widgets/containers/content_header.ts b/apps/client/src/widgets/containers/content_header.ts index ac001d40a..96b9ba5b8 100644 --- a/apps/client/src/widgets/containers/content_header.ts +++ b/apps/client/src/widgets/containers/content_header.ts @@ -2,15 +2,19 @@ import { EventData } from "../../components/app_context"; import BasicWidget from "../basic_widget"; import Container from "./container"; import NoteContext from "../../components/note_context"; +import "./content_header.css"; export default class ContentHeader extends Container { - + noteContext?: NoteContext; thisElement?: HTMLElement; parentElement?: HTMLElement; resizeObserver: ResizeObserver; currentHeight: number = 0; currentSafeMargin: number = NaN; + previousScrollTop: number = 0; + isFloating: boolean = false; + scrollThreshold: number = 10; // pixels before triggering float constructor() { super(); @@ -35,7 +39,36 @@ export default class ContentHeader extends Container { this.thisElement = this.$widget.get(0)!; this.resizeObserver.observe(this.thisElement); - this.parentElement.addEventListener("scroll", this.updateSafeMargin.bind(this)); + this.parentElement.addEventListener("scroll", this.updateScrollState.bind(this), { passive: true }); + } + + updateScrollState() { + const currentScrollTop = this.parentElement!.scrollTop; + const isScrollingUp = currentScrollTop < this.previousScrollTop; + const hasMovedEnough = Math.abs(currentScrollTop - this.previousScrollTop) > this.scrollThreshold; + + if (hasMovedEnough) { + this.setFloating(isScrollingUp); + this.previousScrollTop = currentScrollTop; + } + + this.updateSafeMargin(); + } + + setFloating(shouldFloat: boolean) { + if (shouldFloat !== this.isFloating) { + this.isFloating = shouldFloat; + + if (shouldFloat) { + this.$widget.addClass("floating"); + // Set CSS variable so ribbon can position itself below the floating header + this.parentElement!.style.setProperty("--content-header-height", `${this.currentHeight}px`); + } else { + this.$widget.removeClass("floating"); + // Reset CSS variable when header is not floating + this.parentElement!.style.removeProperty("--content-header-height"); + } + } } updateSafeMargin() { @@ -60,4 +93,4 @@ export default class ContentHeader extends Container { } } -} \ No newline at end of file +} diff --git a/apps/client/src/widgets/ribbon/style.css b/apps/client/src/widgets/ribbon/style.css index 290d1b30e..a14d5bc96 100644 --- a/apps/client/src/widgets/ribbon/style.css +++ b/apps/client/src/widgets/ribbon/style.css @@ -1,5 +1,15 @@ .ribbon-container { margin-bottom: 5px; + position: relative; + transition: position 0.3s ease, z-index 0.3s ease, top 0.3s ease; + z-index: 8; +} + +/* When content header is floating, ribbon sticks below it */ +.scrolling-container:has(.content-header-widget.floating) .ribbon-container { + position: sticky; + top: var(--content-header-height, 100px); + z-index: 10; } .ribbon-top-row { @@ -404,4 +414,4 @@ body[dir=rtl] .attribute-list-editor { background-color: transparent !important; pointer-events: none; /* makes it unclickable */ } -/* #endregion */ \ No newline at end of file +/* #endregion */